Browser-apis
Data Attributes and the dataset API
Definition
- Custom data attributes start with
data-and are accessible through thedatasetproperty.
Reading Data Attributes
<div
id="user"
data-user-id="123"
data-role="admin"
data-is-active="true"
>
John Doe
</div>
const user = document.querySelector('#user')
// Read data attributes (camelCase!)
console.log(user.dataset.userId) // "123"
console.log(user.dataset.role) // "admin"
Adding or Updating Data Attributes
user.dataset.lastLogin = '2024-01-15'
// Creates:
// data-last-login="2024-01-15"
Deleting Data Attributes
delete user.dataset.role
Event Delegation in JavaScript
Definition
- Event delegation is a technique where, instead of attaching event listeners to multiple child elements, you attach a single event listener to a parent element and use
event.targetto determine which child triggered the event. - This approach:
- Uses less memory.
- Scales better when working with many elements.
- Works for elements added later without attaching new event listeners.
Example 1
- Adding event listeners to every item does not scale.
// HTML:
// <ul id="menu">
// <li class="todo-item"><button>Click</button></li>
// </ul>
document.querySelectorAll('.li').forEach(item => {
item.addEventListener('click', handleClick)
})
// Items added later won't have event listeners.
The Solution
Attach a single event listener to the parent element.
document.querySelector('.menu').addEventListener('click', (event) => {
// event.target.tagName -> The element that was clicked.
// event.currentTarget.tagName -> The element where the listener is attached.
// matches() only works if the clicked element itself matches '.todo-item'.
// It won't match if, for example, a button inside the <li> is clicked.
if (event.target.matches('.todo-item')) {
handleClick(event)
}
})
Example 2: Element.closest()
Definition: closest()
- The
closest()method traverses up the DOM tree to find the nearest ancestor (or the element itself) that matches a selector.
// HTML:
//
// <ul id="todo-list">
// <li data-id="1">Buy groceries</li>
// <li data-id="2">Walk the dog</li>
// <li data-id="3">Finish report</li>
// </ul>
const todoList = document.getElementById('todo-list')
todoList.addEventListener('click', (event) => {
// Works regardless of where inside the <li> the user clicks.
const item = event.target.closest('li')
if (item) {
const id = item.dataset.id
console.log(`Clicked todo item with id: ${id}`)
item.classList.toggle('completed')
}
})
Custom Events
What Are Custom Events?
- Custom events let you create your own event types, attach any data you want, and build applications where components communicate through events instead of direct function calls.
- Use cases:
- Notify the application when a modal opens or closes. Listeners then can
- Pause background videos.
- Disable page scrolling.
- Shopping Cart Updates, Instead of updating multiple UI elements manually. Listeners then can
- Update the cart badge.
- Recalculate totals.
- A notification component listens and displays the message.
- Components can update app theme themselves without knowing who initiated the change.
- Notify the application when a modal opens or closes. Listeners then can
Example 1
const event = new CustomEvent('userLoggedIn', {
detail: {
username: 'alice',
timestamp: Date.now()
}
})
// Listen for the event anywhere in your app
document.addEventListener('userLoggedIn', (e) => {
console.log(`Welcome, ${e.detail.username}!`)
})
// Dispatch the event
document.dispatchEvent(event)
// Output:
// Welcome, alice!
Observer APIs
Intersection Observer
Definition
- The Intersection Observer API lets you detect when an element enters, exits, or crosses a specified visibility threshold within a viewport or container element.
- Performance
- Runs off the main thread.
- Optimized by the browser, making it much more efficient.
- Use Cases
- Lazy-load images only when they intersect with the viewport.
- Build infinite scrolling by loading new items when the bottom is/about reached.
- Trigger scroll-based animations when elements enter the viewport.
Example
// Lazy-load images when they come into view
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target
img.src = img.dataset.src
// Never forget to disconnect observers, which can lead to memory leaks.
observer.unobserve(img)
}
})
})
document
.querySelectorAll('img[data-src]')
.forEach(img => observer.observe(img))
MutationObserver
Definition
MutationObserverwatches for DOM changes. It fires a callback when elements are added or removed, attributes change, or text content changes.MutationObserveris asynchronous and batches changes.- Use Cases
- Automatically highlight code blocks added to the page.
- Block ads or unwanted elements injected by third-party scripts.
- Detect when form content changes and trigger auto-save.
Example 1
// Watch for any changes to a DOM element
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === 'childList') {
console.log('Children changed!')
console.log('Added:', mutation.addedNodes)
console.log('Removed:', mutation.removedNodes)
}
if (mutation.type === 'attributes') {
console.log(`Attribute "${mutation.attributeName}" changed`)
}
}
})
const targetElement = document.getElementById('app')
observer.observe(targetElement, {
childList: true, // Watch for added/removed children
attributes: true, // Watch for attribute changes
characterData: true // Watch for text content changes
})
Example 2: Blocking Injected Elements
const adBlocker = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.nodeType !== Node.ELEMENT_NODE) continue
if (node.matches('.ad-banner, [data-ad], .sponsored')) {
node.remove()
console.log('Blocked unwanted element')
}
}
}
})
adBlocker.observe(document.body, {
childList: true,
subtree: true
})
ResizeObserver
Definition
- The ResizeObserver API lets you watch elements for size changes and react accordingly.
- Use Cases
- Adjust font size based on container width without media queries.
- Keep a canvas sharp at any size by matching its internal resolution.
- Keep a chat window scrolled to the bottom when new messages arrive.
- Maintain the aspect ratio of responsive video or image containers.
Example
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
console.log('Element resized:', entry.target)
console.log('New width:', entry.contentRect.width)
console.log('New height:', entry.contentRect.height)
}
})
observer.observe(document.querySelector('.resizable-box'))
PerformanceObserver
Definition
- The Performance Observer API lets you monitor performance metrics in real time. Instead of polling for data, you subscribe to specific performance events and get notified when they occur.
- Used By
- Tools like Google Analytics to measure Core Web Vitals.
Some Entry Types
| Entry Type | Description | Use |
|---|---|---|
navigation | Page navigation timing | Measure page load performance |
resource | Network requests for scripts, styles, images, etc. | Track asset loading times |
paint | First Paint (FP) and First Contentful Paint (FCP) | Track rendering milestones |
largest-contentful-paint | LCP metric (Core Web Vital) | Measure loading performance |
The buffered: true Option
- The
bufferedoption captures performance entries that occurred before the observer started listening.
Example
// Create an observer with a callback function
const observer = new PerformanceObserver((list, observer) => {
// Called whenever new performance entries are recorded
const entries = list.getEntries()
entries.forEach((entry) => {
console.log(`Entry type: ${entry.entryType}`)
console.log(`Name: ${entry.name}`)
console.log(`Start time: ${entry.startTime}`)
console.log(`Duration: ${entry.duration}`)
})
})
// Start observing specific entry types
observer.observe({
entryTypes: ['resource', 'navigation'],
buffered: true
})
Blob and File API
Blob
Definition
- A Blob (Binary Large Object) is an immutable, file-like object that represents raw binary data.
Example
// Creating Blobs from different data types
const textBlob = new Blob(['Hello, World!'], {
type: 'text/plain'
})
const jsonBlob = new Blob(
[JSON.stringify({ name: 'Alice' })],
{ type: 'application/json' }
)
const htmlBlob = new Blob(['<h1>Title</h1>'], {
type: 'text/html'
})
console.log(textBlob.size) // 13 (bytes)
console.log(textBlob.type) // "text/plain"
- You can create a url from a blob
- You need to clean the object with
revokeObjectURL
- You need to clean the object with
const url = URL.createObjectURL(jsonBlob)
const link = document.createElement('a')
link.href = url
link.download = 'hello.txt'
link.click()
URL.revokeObjectURL(url) // Clean up memory
File API
Getting Files from User Input
// HTML:
// <input type="file" id="fileInput" multiple>
const fileInput = document.getElementById('fileInput')
fileInput.addEventListener('change', (event) => {
const files = event.target.files // FileList object
for (const file of files) {
console.log('Name:', file.name) // "photo.jpg"
console.log('Size:', file.size) // 1024000 (bytes)
console.log('Type:', file.type) // "image/jpeg"
console.log('Modified:', file.lastModified) // 1704067200000 (timestamp)
console.log('Modified Date:', new Date(file.lastModified))
}
})
Creating File Objects Programmatically
- A File is a Blob that represents an actual file.
- A File inherits from Blob and adds file-specific metadata.
- Addition metadata File adds include
file.namefile.lastModified
// Syntax:
// new File(fileBits, fileName, options)
const file = new File(
['Hello, World!'], // Content (same as Blob)
'greeting.txt', // File name
{
type: 'text/plain', // MIME type
lastModified: Date.now() // Optional timestamp
}
)
console.log(file.name) // "greeting.txt"
console.log(file.size) // 13
console.log(file.type) // "text/plain"
Uploading A File in Chunks
async function uploadInChunks(file, chunkSize = 1024 * 1024) { // 1 MB chunks
const totalChunks = Math.ceil(file.size / chunkSize)
for (let i = 0; i < totalChunks; i++) {
const start = i * chunkSize
const end = Math.min(start + chunkSize, file.size)
const chunk = file.slice(start, end)
const formData = new FormData()
formData.append('chunk', chunk)
formData.append('chunkIndex', i)
formData.append('totalChunks', totalChunks)
formData.append('filename', file.name)
await fetch('/api/upload-chunk', {
method: 'POST',
body: formData
})
console.log(`Uploaded chunk ${i + 1}/${totalChunks}`)
}
}
Web Worker
- Mozilla Web Workers documentation
- Web Workers are a simple means for web content to run scripts in background threads. The worker thread can perform tasks without interfering with the user interface.
- In simple terms, web Workers give you parallelism, while async/await and Promises give you concurrency
- What you can and can't inside web workers
- You can run whatever code you like inside the worker thread, with some exceptions.
- For example, you can't directly manipulate the
DOMfrom inside a worker, or use some default methods and properties of the window object
- For example, you can't directly manipulate the
- You can use a large number of items available under window, including
WebSockets, and data storage mechanisms likeIndexedDB.
- You can run whatever code you like inside the worker thread, with some exceptions.
- There are different types of Web workers with different purposes
- Dedicated Workers
- Background computation, within a single tab
- Shared Workers
- Shared computation across tabs
- Service Workers
- Network proxy, offline
- Dedicated Workers
Create a Web Worker
const worker = new Worker('worker-file.js');
Send Message & Responses
- Both the client and server use
postMessageto send messages & responses to each other- The client uses it to send request
- The server uses it to send response
const worker = new Worker('worker.js', { type: 'module' })
worker.postMessage({ task: 'process', data: [1, 2, 3] })
worker.onmessage = (event) => {
console.log('Result:', event.data)
}
worker.onerror = (event) => {
console.error('Worker error:', event.message)
}
Receive Message & Response
- Both client and server respond to messages via the
onmessageevent handler- The message is contained within the message event's data attribute.
- The data is copied rather than shared, which avoid traditional race conditions issues.
- If you have a large data like image which you don't want to be copied, you can use a more efficient
Transfer Ownershipoption to transfer the objects ownership instead of copy transfer.
- If you have a large data like image which you don't want to be copied, you can use a more efficient
// worker.js
// loads script (old way), processData comes from global scope
importScripts('./utils.js')
// Standard ES modules! (modern way)
import { processData } from './utils.js'
self.onmessage = (event) => {
const { task, data } = event.data
if (task === 'process') {
const result = processData(data)
self.postMessage(result)
}
}
Scheduling APIs
requestAnimationFrame
Definition
requestAnimationFrame(often abbreviated as rAF) is a browser API that tells the browser you want to perform an animation.- It requests a callback to be executed just before the browser performs its next repaint, typically at 60 frames per second (60 FPS) on most displays.
- Note
requestAnimationFrameis one-shot by design. You must callrequestAnimationFrame()inside the callback to continue the animation.
Example
// The browser calls this function when it's ready to paint
function drawFrame(timestamp) {
// timestamp = milliseconds since page load
console.log(`Frame at ${timestamp}ms`)
// Do your animation work here
updatePosition()
// Request the next frame
requestAnimationFrame(drawFrame)
}
// Start the animation loop
requestAnimationFrame(drawFrame)
Example
const box = document.getElementById('box');
let position = 0;
function animate() {
position += 2; // move 2px per frame
box.style.left = position + 'px';
// stop condition
if (position < 500) {
requestAnimationFrame(animate);
}
}
// start animation
requestAnimationFrame(animate);
requestIdleCallback
requestIdleCallback()lets the browser run work when it is idle and not busy handling:- User input
- Rendering
- Animations
- Higher-priority tasks
Example:
requestIdleCallback(() => {
expensiveAnalyticsWork();
});
Useful for:
- Analytics
- Logging
- Prefetching data
- Non-critical computations
Example with remaining idle time:
requestIdleCallback((deadline) => {
while (deadline.timeRemaining() > 0) {
processNextItem();
}
});
Example with timeout:
requestIdleCallback(
() => {
saveAnalytics();
},
{ timeout: 2000 }
);
This guarantees the callback runs within ~2 seconds even if the browser never becomes idle.
Caveats
- Not supported in all browsers.
- Never use for critical UI updates.
- Browser decides when "idle" time exists.
For animation work, prefer:
requestAnimationFrame()
For background/non-urgent work, prefer:
requestIdleCallback()